package edu.scau.ojforwarder.util;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.scau.ojforwarder.bean.NowcoderError;
import edu.scau.ojforwarder.bean.NowcoderSubmission;
import edu.scau.ojforwarder.bean.ProblemInfo;
import edu.scau.ojforwarder.bean.NowcoderStatus;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.http.util.TextUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;


@Component
public class NowcoderClient {

    @Value("${nowcoder.connectTimeout:0}")
    private int connectTimeout;
    @Value("${nowcoder.socketTimeout:0}")
    private int socketTimeout;
    @Value("${nowcoder.username}")
    private String username;
    @Value("${nowcoder.password}")
    private String password;

    /* ContestId -> ProblemNum -> ProblemInfo */
    private final Map<String, Map<String, ProblemInfo>> problemInfoMap = new HashMap<>();

    private Logger logger = LoggerFactory.getLogger(NowcoderClient.class);

    /**
     * 登录牛客网
     * @throws Exception 网络不通或账号密码不正确时抛出异常
     */
    private void login() throws ErrorException {
        if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password))
            throw new ErrorException(NowcoderError.LOGIN_FAILED);
        HttpResponse response;
        try {
            response = request(RequestMethod.POST,"https://www.nowcoder.com/login/do").bodyForm(
                    new BasicNameValuePair("email", username),
                    new BasicNameValuePair("pwd", password),
                    new BasicNameValuePair("remember", "true")
            ).execute().returnResponse();
        } catch (Exception e) {
            logger.error(NowcoderError.CONNECTION_ERROR.getMsg(), e);
            throw new ErrorException(NowcoderError.CONNECTION_ERROR);
        }
        if (response.getStatusLine().getStatusCode() == 200) {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode node;
            try {
                node = mapper.readTree(EntityUtils.toString(response.getEntity()));
            } catch (IOException e) {
                logger.error(NowcoderError.LOGIN_UNKONWN_ERROR.getMsg(), e);
                throw new ErrorException(NowcoderError.LOGIN_UNKONWN_ERROR);
            }
            if (node.get("code").asInt() == 0)
                logger.info("牛客网登录成功！");
            else {
                logger.error(NowcoderError.LOGIN_FAILED + "[username: " + username + ", password: " + password + "]");
                throw new ErrorException(NowcoderError.LOGIN_FAILED);
            }
        } else {
            logger.error(NowcoderError.CONNECTION_ERROR.getMsg() + "[HTTP状态码：" + response.getStatusLine().getStatusCode() + "]");
            throw new ErrorException(NowcoderError.CONNECTION_ERROR);
        }
    }


    /**
     * 提交代码
     * @param submission 需要提交的代码
     * @return runId
     * @throws Exception 提交失败
     */
    public String submit(NowcoderSubmission submission) throws ErrorException {
        return submit(submission, true);
    }

    /**
     * 提交代码
     * @param tryAgain 失败是否重试
     */
    private String submit(NowcoderSubmission submission, boolean tryAgain) throws ErrorException {

        if (!problemInfoMap.containsKey(submission.getContestId()) ||
                !problemInfoMap.get(submission.getContestId()).containsKey(submission.getProblemNum()))
            fetchProblemInfo(submission.getContestId(), submission.getProblemNum(), true);
        ProblemInfo problemInfo = problemInfoMap.get(submission.getContestId()).get(submission.getProblemNum());
        String response;
        try {
            response = request(RequestMethod.POST, "https://www.nowcoder.com/submit_cd?").bodyForm(
                    new BasicNameValuePair("questionId", problemInfo.getQuestionId()),
                    new BasicNameValuePair("content", submission.getContent()),
                    new BasicNameValuePair("language", submission.getLanguage()),
                    new BasicNameValuePair("tagId", problemInfo.getTagId()),
                    new BasicNameValuePair("doneQuestionId", problemInfo.getDoneQuestionId())
            ).execute().returnContent().asString();
        } catch (Exception e) {
            throw new ErrorException(NowcoderError.CONNECTION_ERROR);
        }

        ObjectMapper mapper = new ObjectMapper();
        JsonNode node;
        try {
            node = mapper.readTree(response);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
            throw new ErrorException(NowcoderError.UNKNOWN_ERROR);
        }

        int code = node.get("code").asInt();
        if (code == 0)
            return Long.toString(node.get("submissionId").asLong());
        else if (code == 1) {
            if (tryAgain) {
                login();
                return submit(submission, false);
            }
            else
                throw new ErrorException(NowcoderError.NOT_LOGGED_ERROR);
        } else
            throw new ErrorException(NowcoderError.OTHER_SUBMIT_EROOR);
    }

    /**
     * 获取题目信息
     * @param contestId
     * @param problemNum
     * @return
     * @throws Exception
     */
    public ProblemInfo getProblemInfo(String contestId, String problemNum) throws ErrorException {
        synchronized (problemInfoMap) {
            if (problemInfoMap.containsKey(contestId) && problemInfoMap.get(contestId).containsKey(problemNum))
                return problemInfoMap.get(contestId).get(problemNum);
        }
        fetchProblemInfo(contestId, problemNum, true);
        synchronized (problemInfoMap) {
            return problemInfoMap.get(contestId).get(problemNum);
        }
    }

    /**
     * 下载题目页面信息
     * @param contestId
     * @param problemNum
     * @param tryAgain
     * @throws Exception
     */
    private void fetchProblemInfo(String contestId, String problemNum, boolean tryAgain) throws ErrorException {
        String html;
        try {
            html = request(RequestMethod.GET, "https://www.nowcoder.com/acm/contest/" + contestId + "/" + problemNum).execute()
                    .returnContent().asString();
        } catch (Exception e) {
            logger.error("获取牛客网题目信息出错。", e);
            throw new ErrorException(NowcoderError.CONNECTION_ERROR);
        }
        if (html.contains("tab-login")) {
            if (tryAgain) {
                login();
                fetchProblemInfo(contestId, problemNum, false);
            } else
                throw new ErrorException(NowcoderError.NOT_LOGGED_ERROR);
        } else if (!html.contains("terminal-topic")) {
            throw new ErrorException(NowcoderError.NO_SUCH_PROBLEM);
        } else {
            Document document = Jsoup.parse(html);
            document.outputSettings().prettyPrint(false);
            Element descElement = document.getElementsByClass("terminal-topic").first();
            descElement.getElementsByClass("code-copy-btn").remove();
            String desc = descElement.outerHtml();
            StringBuffer buffer = new StringBuffer();
            int pos = html.indexOf("pageInfo");
            while (html.charAt(pos) != '{')
                pos++;
            while (html.charAt(pos - 1) != '}')
                buffer.append(html.charAt(pos++));
            ObjectMapper mapper = new ObjectMapper();
            mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);  //允许key没有引号
            mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);         //允许单引号
            mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);              //允许评论
            JsonNode infoJson;
            try {
                infoJson = mapper.readTree(buffer.toString());
            } catch (IOException e) {
                logger.error(NowcoderError.UNKNOWN_ERROR.getMsg(), e);
                throw new ErrorException(NowcoderError.UNKNOWN_ERROR);
            }
            ProblemInfo problemInfo = new ProblemInfo();
            problemInfo.setContestId(contestId);
            problemInfo.setProblemNum(problemNum);
            problemInfo.setDescription(desc);
            problemInfo.setQuestionId(infoJson.get("questionId").asText());
            problemInfo.setDoneQuestionId(infoJson.get("doneQuestionId").asText());
            problemInfo.setTagId(infoJson.get("tagId").asText());
            synchronized (problemInfoMap) {
                Map<String, ProblemInfo> contest = problemInfoMap.computeIfAbsent(contestId, k -> new HashMap<>());
                contest.put(problemNum, problemInfo);
            }
        }
    }

    /**
     * 从牛客网获取评测结果
     * @param runId 牛客网返回的runId
     * @return 评测结果
     * @throws ErrorException
     */
    public NowcoderStatus getStatus(String runId) throws ErrorException {
        String content;
        try {
            content = request(RequestMethod.GET,"https://www.nowcoder.com/status?submissionId="+runId+"&_="+ System.currentTimeMillis()).execute().returnContent().asString();
        } catch (Exception e) {
            throw new ErrorException(NowcoderError.CONNECTION_ERROR);
        }
        ObjectMapper mapper = new ObjectMapper();
        JsonNode result;
        try {
            result = mapper.readTree(content);
        } catch (IOException e) {
            logger.error(NowcoderError.UNKNOWN_ERROR.getMsg(), e);
            throw new ErrorException(NowcoderError.UNKNOWN_ERROR);
        }
        NowcoderStatus status = new NowcoderStatus();
        status.setRunId(runId);
        status.setDesc(result.get("desc").asText());
        status.setMemo(result.get("memo").asText());
        switch (result.get("status").asInt()) {
            case 0: status.setStatus(NowcoderStatus.QUEUING); break;
            case 3: status.setStatus("RE"); break;
            case 4: status.setStatus("WA"); break;
            case 5: status.setStatus("AC"); status.setExeMemory("" + result.get("memory").asInt());
                status.setExeTime("" + result.get("seconds").asInt()); break;
            case 6: status.setStatus("TLE"); break;
            case 7: status.setStatus("MLE"); break;
            case 8: status.setStatus("OLE"); break; //Ouptut Limit Exceeded 输出超出限制
            case 12: status.setStatus("CE"); break;
            case 13: status.setStatus("PE"); break;
            default: status.setStatus("OtherError");
        }
        return status;
    }

    public Request request(RequestMethod method, String url) {
        Request request;
        if (RequestMethod.POST == method)
            request = Request.Post(url);
        else
            request = Request.Get(url);
        return request.connectTimeout(connectTimeout).socketTimeout(socketTimeout);
    }
}
